package com.tarena.lbs.marketing.web.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.tarena.lbs.attach.api.AttachApi;
import com.tarena.lbs.base.common.utils.Asserts;
import com.tarena.lbs.base.protocol.exception.BusinessException;
import com.tarena.lbs.base.protocol.pager.PageResult;
import com.tarena.lbs.basic.api.BasicApi;
import com.tarena.lbs.common.passport.principle.UserPrinciple;
import com.tarena.lbs.marketing.web.repository.ActivityRepository;
import com.tarena.lbs.marketing.web.repository.CouponCodeRepository;
import com.tarena.lbs.marketing.web.repository.CouponRepository;
import com.tarena.lbs.marketing.web.repository.UserCouponsRepository;
import com.tarena.lbs.marketing.web.utils.AuthenticationContextUtils;
import com.tarena.lbs.pojo.attach.dto.AttachQrDTO;
import com.tarena.lbs.pojo.attach.param.AttachQRParam;
import com.tarena.lbs.pojo.basic.dto.UserDTO;
import com.tarena.lbs.pojo.marketing.param.UserCouponsParam;
import com.tarena.lbs.pojo.marketing.po.ActivityPO;
import com.tarena.lbs.pojo.marketing.po.CouponCodePO;
import com.tarena.lbs.pojo.marketing.po.CouponPO;
import com.tarena.lbs.pojo.marketing.po.UserCouponsPO;
import com.tarena.lbs.pojo.marketing.query.UserCouponCodeQuery;
import com.tarena.lbs.pojo.marketing.query.UserCouponQuery;
import com.tarena.lbs.pojo.marketing.vo.UserCouponsVO;
import com.tarena.lbs.stock.api.StockApi;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.common.utils.StringUtils;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.rpc.RpcException;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors;

@Service
@Slf4j
public class UserCouponsService {
    @DubboReference
    private BasicApi basicApi;
    @Autowired
    private ActivityRepository activityRepository;
    @Autowired
    private CouponRepository couponRepository;
    @Autowired
    private UserCouponsRepository userCouponsRepository;
    @Autowired
    private CouponCodeRepository couponCodeRepository;
    @DubboReference
    private StockApi stockApi;
    @Autowired
    private RedisTemplate redisTemplate;
    @Transactional(rollbackFor = Exception.class)
    public void receiveCoupon(UserCouponsParam param) throws BusinessException, ExecutionException, InterruptedException {
        long threadId = Thread.currentThread().getId();
        log.info("用户领取优惠券业务方法,线程ID:{}",threadId);

        //1.使用param中携带的userId 验证用户合法
        CompletableFuture<UserDTO> futureUser = CompletableFuture.supplyAsync(() -> {
            try {
                return checkUser(param.getUserId());
            } catch (BusinessException e) {
                throw new RuntimeException(e);
            }
        });

        //2.使用param中的userId和couponId,验证活动 用户匹配度
        CompletableFuture<ActivityPO> futureActivity = CompletableFuture.supplyAsync(() -> {
            try {
                return checkAndGetActivity(param);
            } catch (BusinessException e) {
                throw new RuntimeException(e);
            }
        });
        //3.使用param中的couponId 和userId 检查优惠券 领取上限
        CompletableFuture<CouponPO> futureCoupon = CompletableFuture.supplyAsync(() -> {
            try {
                return checkAndGetCoupon(param);
            } catch (BusinessException e) {
                throw new RuntimeException(e);
            }
        });
        //如果不使用返回值,当前业务方法执行时间接近0ms 但是需要等待 实现一个阻塞等待
        CompletableFuture<Void> allFutures = CompletableFuture.allOf(futureUser, futureActivity, futureCoupon);
        //从allFutures内部回调 获取每个方法的返回值
        CouponPO couponPO = futureCoupon.get();
        ActivityPO activityPO = futureActivity.get();
        UserDTO userDTO = futureUser.get();
        //4.执行领取优惠券 检查扣减库存充足couponId 生成券码couponId 领取信息 couponCode activityId couponId
        //5.添加锁的逻辑 操作string类型数据结构
        ValueOperations<String,String> opsForValue = redisTemplate.opsForValue();
        String lockKey="lbs:marketing:coupon:"+param.getCouponId();
        String randCode=UUID.randomUUID().toString();
        //setnx key value EX 5
        //do while循环 进来先抢锁 判断while是没抢到就抢
        Boolean result=false;
        int count=0;
        do{
            if (count>0){
                //当前线程不是第一次抢锁 需要等待
                try{
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    log.error("线程:{},等待锁释放",Thread.currentThread().getId());
                }
            }/*else if (count>5){
                throw new BusinessException("-2","循环次数太多,并发争抢失败");
            }*/
            result= opsForValue.setIfAbsent(lockKey, randCode,5, TimeUnit.SECONDS);
            count++;
        }while(!result);
        try {
            receiveCoupon(param, couponPO);
        }catch (Exception e){
            throw new BusinessException("-2",e.getMessage());
        }finally{
            //释放锁
            String value = opsForValue.get(lockKey);
            if (value!=null&&value.equals(randCode)) {
                redisTemplate.delete(lockKey);
            }
        }

    }

    private CouponPO checkAndGetCoupon(UserCouponsParam param) throws BusinessException {
        long threadId = Thread.currentThread().getId();
        log.info("检查优惠券,线程ID:{}",threadId);
        long start=System.currentTimeMillis();
        //1.使用参数中couponId查询已有的couponPO
        CouponPO couponPO = couponRepository.getById(param.getCouponId());
        //2.断言存在
        Asserts.isTrue(couponPO==null,new BusinessException("-2","优惠券数据不存在"));
        //3.读取当前用户 (param.getUserId()) 已经领取该优惠券多少次了.判断是否达到领取上限
        //select count from user_coupons where user_id=? and coupon_id=?
        QueryWrapper queryWrapper=new QueryWrapper();
        queryWrapper.eq("user_id",param.getUserId());
        queryWrapper.eq("coupon_id",param.getCouponId());
        //当前用户已经领取的次数
        long receiveTimes = userCouponsRepository.count(queryWrapper);
        Asserts.isTrue(receiveTimes>=couponPO.getUsageLimit(),new BusinessException("-2","您已经领取过多"));
        long end = System.currentTimeMillis();
        log.info("优惠券信息检查耗时:{}MS",end-start);
        return couponPO;
    }

    private ActivityPO checkAndGetActivity(UserCouponsParam param) throws BusinessException {
        long threadId = Thread.currentThread().getId();
        log.info("检查活动,线程ID:{}",threadId);
        long start = System.currentTimeMillis();
        //1.使用id查询activity是否存在
        ActivityPO activityPO = activityRepository.getById(param.getActivityId());
        Asserts.isTrue(activityPO==null,new BusinessException("-2","活动数据不存在"));
        //2.判断活动时间是否 startDate<now<endDate
        Date now=new Date();
        boolean isTimeOK=now.before(activityPO.getEndDate())&&now.after(activityPO.getStartDate());
        Asserts.isTrue(!isTimeOK,new BusinessException("-2","活动时间不正确"));
        //3.验证活动中 目标人群是否包含当前登录用户
        Integer targetId=Integer.valueOf(activityPO.getTargetCustomer());
        List<Integer> userGroupIds = basicApi.getUserGroupIds(param.getUserId(), activityPO.getBusinessId());
        Asserts.isTrue(!userGroupIds.contains(targetId),new BusinessException("-2","活动不在目标人群"));
        long end = System.currentTimeMillis();
        log.info("活动信息检查耗时:{}MS",end-start);
        return activityPO;
    }

    private UserDTO  checkUser(Integer userId) throws BusinessException {
        long threadId = Thread.currentThread().getId();
        log.info("检查用户,线程ID:{}",threadId);
        long start = System.currentTimeMillis();
        //判断用户是否存在 是否状态可领状态 如果禁用 冻结
        UserDTO userDTO = basicApi.userDetail(userId);
        Asserts.isTrue(userDTO==null,new BusinessException("-2","领取优惠券用户不存在"));
        long end = System.currentTimeMillis();
        log.info("用户信息检查耗时:{}MS",end-start);
        return userDTO;
    }

    public void receiveCoupon(UserCouponsParam param,CouponPO poParam) throws BusinessException {
        //1.根据入参 检查当前优惠券剩余库存是否还足够领取 因为每个用户领取只有一张
        //所以只要剩余库存大于1 就算是足够的
        checkStockEnough(poParam.getId());
        //2.给当前用户生成可领的一张优惠券券码 需要什么参数 刚创建的时候状态未领取
        CouponCodePO codePo=createCouponCode(poParam);
        //3.记录领取信息 user_coupons
        createUserCoupons(param,poParam,codePo);
        //4.修改券码状态 从未领取 到已领取
        updateCouponCodeStatus(codePo.getId(),1);
        //5.扣减库存
        reduceCouponStock(poParam.getId());
    }

    private void reduceCouponStock(Integer couponId) throws BusinessException {
        //将这个优惠券库存扣减1个
        log.info("开始扣减库存,优惠券ID:{}",couponId);
        boolean result=false;
        try {
            result = stockApi.reduceCouponStock(couponId, 1);
        }catch (RpcException e){
            log.error("扣减优惠券:{},库存个数:{},失败",couponId,1,e);
            throw new BusinessException("-2","扣减库存远程调用失败");
        }
        if (!result){
            throw new BusinessException("-2","扣减库存失败");
        }
    }

    private void updateCouponCodeStatus(Integer id, Integer status) {
        //update coupon_code set status=?,set update_at=? where id=?
        CouponCodePO updatePO=new CouponCodePO();
        updatePO.setStatus(status);
        updatePO.setUpdateAt(new Date());
        updatePO.setId(id);
        couponCodeRepository.updateById(updatePO);
    }

    private void createUserCoupons(UserCouponsParam param, CouponPO coupon, CouponCodePO code) {
        UserCouponsPO po=new UserCouponsPO();
        po.setCouponValue(coupon.getDiscountValue());
        po.setCouponId(coupon.getId());
        po.setUserId(param.getUserId());
        po.setCouponCode(code.getCouponCode());
        po.setActivityId(param.getActivityId());
        po.setStatus(0);//status 0未使用 1已使用 2已过期
        po.setShopId(param.getShopId());
        po.setReceiveChannel(1);//1页面嵌入领取 2主动投放消息 3自动营销
        //po.setOrderNo();等待优惠券使用的时候 更新
        //po.setUsedTime();下单扫码使用的时候 跟新
        po.setCouponType(coupon.getCouponType());
        po.setCreateAt(new Date());
        po.setUpdateAt(new Date());
        log.info("领取优惠券记录数据封装完毕,记录数据:{}",po);
        userCouponsRepository.save(po);
    }

    private CouponCodePO createCouponCode(CouponPO poParam) {
        CouponCodePO po=new CouponCodePO();
        po.setCouponId(poParam.getId());
        po.setBusinessId(poParam.getBusinessId());
        po.setCouponCode(UUID.randomUUID().toString());
        po.setStatus(0);//初始化状态是0 未分配 1 已分配
        po.setCreateAt(new Date());
        po.setUpdateAt(new Date());
        couponCodeRepository.save(po);
        return po;
    }

    private void checkStockEnough(Integer id) throws BusinessException {
        Integer couponStock = stockApi.getCouponStock(id);
        //没有库存数据 抛异常
        Asserts.isTrue(couponStock==null,new BusinessException("-2","库存不存在检查数据一致性"));
        //库存小于等于0
        Asserts.isTrue(couponStock<=0,new BusinessException("-2","库存不足"));
    }

    public PageResult<UserCouponsVO> userCouponList(UserCouponQuery couponQuery) throws BusinessException {
        //1.解析拿到用户的id
        Integer userId = getUserId();
        //2.查询优惠券列表(优惠券状态0未使用1已使用2已过期)
        List<UserCouponsPO> pos=getUserCoupons(userId,couponQuery.getStatus());
        //3.组织一个假的分页对象
        return assemblePageResult(pos);
    }

    private PageResult<UserCouponsVO> assemblePageResult(List<UserCouponsPO> pos) {
        PageResult<UserCouponsVO> voPage=new PageResult<>(10,1);
        voPage.setTotal(100l);
        List<UserCouponsVO> vos=null;
        if (pos!=null&&pos.size()>0){
            vos=pos.stream().map(po->{
                UserCouponsVO vo=new UserCouponsVO();
                BeanUtils.copyProperties(po,vo);
                return vo;
            }).collect(Collectors.toList());
        }
        voPage.setObjects(vos);
        return voPage;
    }

    private List<UserCouponsPO> getUserCoupons(Integer userId, Integer status) {
        QueryWrapper<UserCouponsPO> queryWrapper=new QueryWrapper<>();
        queryWrapper.eq("user_id",userId);
        queryWrapper.eq("status",status);
        return userCouponsRepository.list(queryWrapper);
    }

    private Integer getUserId() throws BusinessException {
        UserPrinciple userPrinciple = AuthenticationContextUtils.get();
        Asserts.isTrue(userPrinciple==null,new BusinessException("-2","认证解析失败"));
        return userPrinciple.getId();
    }

    public UserCouponsVO userCouponsDetail(UserCouponCodeQuery query) throws BusinessException {
        //1.拿到用户id
        Integer userId = getUserId();
        //2.使用数据层查询该用户 下的该code券码的领取记录详情 一条
        UserCouponsPO po=getUserCoupon(userId,query.getCouponCode());
        //3.封装vo返回 远程调用图片 补充二维码的URL地址
        UserCouponsVO vo=null;
        if (po!=null){
            vo=new UserCouponsVO();
            BeanUtils.copyProperties(po,vo);
            //调用远程图片获取该券的二维码
            String qrURL=getCouponQRUrl(po);
            vo.setCouponUrl(qrURL);
        }
        return vo;
    }
    @DubboReference
    private AttachApi attachApi;
    private String getCouponQRUrl(UserCouponsPO po) {
        ValueOperations<String,String> opsForValue = redisTemplate.opsForValue();
        //调用图片 使用缓存的读流程
        String qrKey="lbs:marketing:coupon:code:"+po.getCouponCode();
        //1 读取缓存
        String qrUrl = opsForValue.get(qrKey);
        if (StringUtils.isNotEmpty(qrUrl)){
            log.info("查询券码:{},缓存命中,url:{}",po.getCouponCode(),qrUrl);
            //缓存命中
            return qrUrl;
        }else{
            log.info("查询券码:{},缓存未命中",po.getCouponCode());
            //缓存未命中,说明是第一次查询图片 调用attach
            AttachQRParam param=new AttachQRParam();
            //content就是生成二维码的主要内容 扫码得到什么 就在content放什么
            String content="https://www.baidu.com?code="+po.getCouponCode();
            param.setContent(content);
            param.setBusinessType(800);
            param.setBusinessId(po.getId());
            AttachQrDTO attachQrDTO = attachApi.generateQrCode(param);//生成二维码 生成二维码图片 返回url 同时绑定业务;
            qrUrl=attachQrDTO.getUrl();
            //保存缓存 方便后续使用
            opsForValue.set(qrKey,qrUrl);
        }
        return qrUrl;
    }

    private UserCouponsPO getUserCoupon(Integer userId, String couponCode) {
        //select * from user_coupons where user_id=? and coupon_code=?
        QueryWrapper<UserCouponsPO> queryWrapper=new QueryWrapper<>();
        queryWrapper.eq("user_id",userId);
        queryWrapper.eq("coupon_code",couponCode);
        return userCouponsRepository.getOne(queryWrapper);
    }
}
